// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include <zencore/enumflags.h>
#include <zencore/logging.h>
#include <zencore/process.h>
#include <zencore/thread.h>
#include <zencore/uid.h>

#include <atomic>
#include <filesystem>
#include <functional>
#include <optional>

namespace zen {

class CbObject;

/** Zen Server Environment configuration

	This class allows a user to configure where a spawned server should place any
	files, and also controls whether it runs as part of a test.

	Especially suited for automated testing scenarios.

 */

class ZenServerEnvironment
{
public:
	ZenServerEnvironment();
	~ZenServerEnvironment();

	void Initialize(std::filesystem::path ProgramBaseDir);
	void InitializeForTest(std::filesystem::path ProgramBaseDir, std::filesystem::path TestBaseDir, std::string_view ServerClass = "");

	std::filesystem::path	CreateNewTestDir();
	std::filesystem::path	ProgramBaseDir() const { return m_ProgramBaseDir; }
	std::filesystem::path	GetTestRootDir(std::string_view Path);
	inline bool				IsInitialized() const { return m_IsInitialized; }
	inline bool				IsTestEnvironment() const { return m_IsTestInstance; }
	inline std::string_view GetServerClass() const { return m_ServerClass; }
	inline uint16_t			GetNewPortNumber() { return m_NextPortNumber.fetch_add(1); }

private:
	std::filesystem::path m_ProgramBaseDir;
	std::filesystem::path m_TestBaseDir;
	bool				  m_IsInitialized  = false;
	bool				  m_IsTestInstance = false;
	std::string			  m_ServerClass;
	std::atomic_uint16_t  m_NextPortNumber{20000};
};

/** Zen Server Instance management

	This class can be used to launch/attach to and manage a running
	Zen server instance

	Especially useful for automated testing but can also be used for
	management tools.

  */
struct ZenServerInstance
{
	ZenServerInstance(ZenServerEnvironment& TestEnvironment);
	~ZenServerInstance();

	int				   Shutdown();
	bool			   SignalShutdown();
	uint16_t		   WaitUntilReady();
	[[nodiscard]] bool WaitUntilReady(int Timeout);
	void			   EnableTermination() { m_Terminate = true; }
	void			   DisableShutdownOnDestroy() { m_ShutdownOnDestroy = false; }
	void			   Detach();
	inline int		   GetPid() { return m_Process.Pid(); }
	inline void		   SetOwnerPid(int Pid) { m_OwnerPid = Pid; }
	bool			   IsRunning();
	bool			   Terminate();
	std::string		   GetLogOutput() const;

	void SetTestDir(std::filesystem::path TestDir);

	inline void SpawnServer(std::string_view AdditionalServerArgs = std::string_view())
	{
		SpawnServer(m_Env.GetNewPortNumber(), AdditionalServerArgs, /* WaitTimeoutMs */ 0);
	}

	inline uint16_t SpawnServerAndWaitUntilReady(std::string_view AdditionalServerArgs = std::string_view())
	{
		SpawnServer(m_Env.GetNewPortNumber(), AdditionalServerArgs, /* WaitTimeoutMs */ 100'000);
		return GetBasePort();
	}

	inline void SpawnServer(int BasePort, std::string_view AdditionalServerArgs = std::string_view())
	{
		SpawnServer(BasePort, AdditionalServerArgs, /* WaitTimeoutMs */ 0);
	}

	inline void SpawnServerAndWait(int BasePort = 0, std::string_view AdditionalServerArgs = std::string_view(), int WaitTimeoutMs = 10000)
	{
		SpawnServer(BasePort, AdditionalServerArgs, WaitTimeoutMs);
	}

	void		AttachToRunningServer(int BasePort = 0);
	std::string GetBaseUri() const;
	uint16_t	GetBasePort() const { return m_BasePort; }

private:
	ZenServerEnvironment&		m_Env;
	ProcessHandle				m_Process;
	NamedEvent					m_ReadyEvent;
	std::unique_ptr<NamedEvent> m_ShutdownEvent;
	bool						m_Terminate			= false;
	bool						m_ShutdownOnDestroy = true;
	std::filesystem::path		m_TestDir;
	uint16_t					m_BasePort = 0;
	std::optional<int>			m_OwnerPid;
	std::string					m_Name;
	std::filesystem::path		m_OutputCapturePath;

	void CreateShutdownEvent(int BasePort);
	void SpawnServer(int BasePort, std::string_view AdditionalServerArgs, int WaitTimeoutMs);
	void OnServerReady();
};

/** Shared system state

	Used as a scratchpad to identify running instances etc

	On Windows, the state lives in a memory-mapped file backed by the swapfile
 */

class ZenServerState
{
public:
	ZenServerState();
	~ZenServerState();

	struct ZenServerEntry
	{
		// NOTE: any changes to this should consider backwards compatibility
		//       which means you should not rearrange members only potentially
		//       add something to the end or use a different mechanism for
		//       additional state. For example, you can use the session ID
		//       to introduce additional named objects
		std::atomic<uint32_t> Pid;
		std::atomic<uint16_t> DesiredListenPort;
		std::atomic<uint16_t> Flags;
		uint8_t				  SessionId[12];
		std::atomic<uint32_t> SponsorPids[8];
		std::atomic<uint16_t> EffectiveListenPort;
		// If you need to add more state, do it here vvvvvvvvvvv
		uint8_t Padding[10];
		// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

		enum class FlagsEnum : uint16_t
		{
			kShutdownPlease = 1 << 0,
			kIsReady		= 1 << 1,
		};

		FRIEND_ENUM_CLASS_FLAGS(FlagsEnum);

		Oid	 GetSessionId() const { return Oid::FromMemory(SessionId); }
		void Reset();
		void SignalShutdownRequest();
		bool IsShutdownRequested() const;
		void SignalReady();
		bool IsReady() const;
		bool AddSponsorProcess(uint32_t Pid, uint64_t Timeout = 0);
	};

	static_assert(sizeof(ZenServerEntry) == 64);

	void						  Initialize();
	[[nodiscard]] bool			  InitializeReadOnly();
	[[nodiscard]] ZenServerEntry* Lookup(int DesiredListenPort) const;
	[[nodiscard]] ZenServerEntry* LookupByEffectivePort(int Port) const;
	ZenServerEntry*				  Register(int DesiredListenPort);
	void						  Sweep();
	void						  Snapshot(std::function<void(const ZenServerEntry&)>&& Callback) const;
	inline bool					  IsReadOnly() const { return m_IsReadOnly; }

private:
	void*			m_hMapFile		= nullptr;
	ZenServerEntry* m_Data			= nullptr;
	int				m_MaxEntryCount = 65536 / sizeof(ZenServerEntry);
	ZenServerEntry* m_OurEntry		= nullptr;
	bool			m_IsReadOnly	= true;
};

struct LockFileInfo
{
	int32_t				  Pid;
	Oid					  SessionId;
	uint16_t			  EffectiveListenPort;
	bool				  Ready;
	std::filesystem::path DataDir;
	std::filesystem::path ExecutablePath;
};

CbObject	 MakeLockFilePayload(const LockFileInfo& Info);
LockFileInfo ReadLockFilePayload(const CbObject& Payload);
bool		 ValidateLockFileInfo(const LockFileInfo& Info, std::string& OutReason);

}  // namespace zen
